[PowerShell] ForEach-Objectの新機能、ForEach-Object -Parallel について
しばたです。
先日リリースされたPowerShell 7 Preview.3では多くの機能追加がされましたが、その中のひとつにForEach-Object
コマンドレットに処理を並列で行う-Parallel
パラメーターの追加があります。
本記事ではこのForEach-Object -Parallel
について解説します。
導入に至る経緯
もともとWindows PowerShellではPowerShell 3.0からワークフローの機能が導入されており、このワークフローの機能のひとつでforeach文に-parallel
パラメーターを指定して並列処理を行うことが可能でした。
# Windows PowerShell 3.0から導入されたワークフロー
workflow Invoke-Parallel {
foreach -parallel ($i in (1..10)) {
"Time {0:HH:mm:ss} | Input : {1}" -f (Get-Date),$i; Start-Sleep -Seconds 5
}
}
Invoke-Parallel
ただ、PowerShell Core 6.0からは.NET Coreでワークフローの基盤となるWorkflow Foundationがサポートされないことからこの機能は廃止されました。
今回のForEach-Object -Parallel
はこのワークフローでの並列処理を代替する目的で導入されています。
前提条件
ForEach-Object -Parallel
を利用するにはPowerShell 7 Preview.3以降の環境が必要です。
この機能は試験的な機能(Experimental Feature)として追加されていますが、PowerShell 7 Preview.3からプレビュー版のPowerShellでは試験的な機能がデフォルトで有効になる仕様変更が入っているため、PowerShell 7 Preview.3インストール後に機能の有効化をする手順は不要です。
試してみる
このForEach-Object -Parallel
では引数に指定されたスクリプトブロックを個別のRunspace(≒スレッド)で並列処理します。
デフォルトは5並列で、-ThrottleLimit
パラメーターを指定することで並列度を変更できます。
また、-TimeoutSeconds
パラメーターを指定すると処理全体でのタイムアウトを指定することができます。
ここで例として以下の様なコマンドを実行すると下図の様な結果となります。
# ForEach-Object -Parallel を使った並列処理 (5並列)
1..10 | ForEach-Object -Parallel { "Time {0:HH:mm:ss} | Input : {1}" -f (Get-Date),$_; Start-Sleep -Seconds 5 }
ここでは最初の5オブジェクト(1
~5
)が並列処理されて5秒間Sleepし、その後残りのオブジェクト(6
~10
)が順に処理されていく形となります。
-Parallel
を指定しない場合はひとつひとつのオブジェクトが順に処理されるので下図の様な結果となります。
こうしてみるとForEach-Object -Parallel
との違いは一目瞭然でしょう。
通常のForEach-Objectとの違い
この結果だけ見ると常にForEach-Object -Parallel
だけ使えば良さそうに見えますが、通常のForEach-Object
では-Begin
、-End
パラメーターを使いコマンドレットのBeginブロック、Endブロックをカスタマイズすることができるのに対して、ForEach-Object -Parallel
では-Begin
、-End
パラメーターは指定できずProcessブロックのみ実行可能となっています。
# 通常の ForEach-Object では Begin / Process / End 各ブロックの処理を記述可能
1..10 | ForEach-Object -Begin { [開始処理] } -Process { [順次処理] } -End { [終了処理] }
# -Parallel パラメーターでは Process ブロックのみ記述可能
1..10 | ForEach-Object -Parallel { [並列処理] }
また、ForEach-Object -Parallel
で並列実行されるスクリプトブロックはRunSpaceが分かれますので外部から変数を引き渡す際はusing:
スコープを指定してやる必要があります。
# スクリプトブロックは別RunSpaceで実行されるため、外部から変数を引き渡す際はusingスコープを指定してやる必要がある
$value = "Exeternal Value"
1..10 | ForEach-Object -Parallel { Write-Output $using:Value }
このことからForEach-Object -Parallel
はForEach-Object
の一つのパラメーターではあるものの処理を実行するコンテキストは通常の場合とは完全に異なり、ほぼ別コマンドレットと考えた方が良いでしょう。
状況に応じて2つの処理を使い分けるのが良いと思います。
実装について
続けてForEach-Object -Parallel
の実装に触れていきます。
この機能は中の人であるPaul Higinbothamさんによって導入され、この方は以前にThreadJobを導入した人でもあります。
このため並列処理の仕組みはThreadJobの方式に似ている部分が多いです。
PSTask・PSTaskPool
ForEach-Object -Parallel
で並列実行される処理はPSTask
と呼ばれる単位で個別のRunSpaceに分かれて実行されます。
PSTaskPool
というクラスが個々のPSTask
をまとめタスクの並列度やタスクに対する終了通知などの役割を担っています。
c#のスレッド処理におけるスレッドプールとTaskの関連をPowerShellのRunSpaceに合わせた形式と考えると分かりやすいでしょう。
ForEach-Object
内部のBeginProcessingメソッドでPSTaskPool
の初期化処理を実施、ProcessRecordメソッドで都度PSTask
を生成しPSTaskPool
に追加しタスク(=-Parallelパラメータで指定したスクリプトブロック)を実行、並列度が-ThrottleLimit
パラメーターで指定されている値を超える場合はPSTaskPool
へのタスク追加がブロックされる仕様となっています。
このためProcessRecordメソッドで実行される処理は並列ですが、開始順は保証されています。
最後にEndProcessingメソッドでPSTaskPool
の終了処理が行われます。
ちなみにPSTask
から各ストリームへの書き込みはPSTaskDataStreamWriter
クラスが調整しています。
概要図
ここまでの説明を図に示すと以下の様になります。
最後に
ざっとこんな感じです。
現在のPowerShell CoreではThreadJobを使うことで同等のことができますが、ForEach-Object -Parallel
はより簡易な記法で取り扱うことができます。
これまで並列処理にはからっきし弱かったPowerShellですがこの機能が導入されることでだいぶ使える様になるのではないかと思います。